package core;

import model.beans.RouterBean;
import model.beans.LinkBean;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;
import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;
import model.beans.OrderBean;
import org.apache.log4j.Logger;
import utility.Utils;

/**
 * This class is just a single instance thread, 
 * which scans a database for incoming (and unprocessed) orders and handles them.
 * @author Danon
 */
@ManagedBean @ApplicationScoped
public class OrderQueue implements Runnable {
    private static final Logger logger = Logger.getLogger(Database.class.getName());
    
    private OrderQueue() {
    }
    
    /** Get instance of the working queue */
    public static synchronized OrderQueue getInstance() {
        if(instance==null) instance = new OrderQueue();
        return instance;
    }
    
    private static OrderQueue instance;
    private Thread thread;
    private boolean running;
    
    @Override
    public void run() {
        running = true;
        logger.info("Order Handler thread started.");
        while(!Thread.interrupted()) {
            OrderBean order = null;
            do {
                order = extractOrder();
            } while(order==null);
            if(order == null) break;
            processOrder(order);
        }
        running = false;
        thread = null;
        logger.info("Order Handler thread terminated.");
    }
    
    /**
     * Processes order, extracted from the database.
     * Which means the following:
     *      1. Set status PROCESSING
     *      2. Execute BFS algorithm
     *      3. Save a result in the database.
     * @param order Order to be processed.
     */
    private void processOrder(OrderBean order) {
        logger.info("Processing order: "+order);
        //set status to "Processing"
        order.setStatus("Processing");
        String status = "PROCESSING";
        try {
            Connection c = Database.getInstance().getConnection();
            try (PreparedStatement ps = c.prepareStatement("update orders set status = ? where order_id = ?")) {
                ps.setInt(2, order.getId()); 
                ps.setString(1, status);
                ps.executeUpdate();
            }
            c.commit();
        } catch (Exception ex) { 
            logger.debug(ex.getMessage()); 
        }
        // find path
        List<LinkBean> result = null;
        try {
            result = Utils.bfs(RouterBean.loadByName(order.getSourceRouterName()), RouterBean.loadByName(order.getDestinationRouterName()), order.getBandwidth());
        } catch (Exception ex) {
            logger.debug("BFS failed!");
        }
        
        if(result == null) {
            status = "FAIL";
            order.setStatus("Fail");
            logger.info("No path");
        }
        else {
            status = "COMPLETED";
            order.setStatus("Completed");
        }
        // set status "Completed" or "Fail"
        
        // save PATH in the database
        // insert into engagements (engagement_id, order_id, link_id, num) values (?, ?, ?, ?)
        if(result!=null && !result.isEmpty()) {
            try {
                Connection c = Database.getInstance().getConnection();
                try (PreparedStatement ps = c.prepareStatement("insert into engagements (engagement_id, order_id, link_id, num) "
                             + "values (gen_engagement_id.nextval, ?, ?, ?)")) {
                    int i = 0;
                    for(LinkBean link : result) {
                        ps.setInt(1, order.getId());
                        ps.setInt(2, link.getId());
                        ps.setInt(3, i++);
                        ps.executeUpdate();
                        logger.info(link);
                    }
                }
                c.commit();
            } catch (Exception ex) { 
                logger.debug(ex.getMessage()); 
            }
        }
        
        try {
            Connection c = Database.getInstance().getConnection();
            try (PreparedStatement ps = c.prepareStatement("update orders set status = ?, completion_date = sysdate where order_id = ?")) {
                ps.setInt(2, order.getId()); 
                ps.setString(1, status);
                ps.executeUpdate();
            }
            c.commit();
        } catch (Exception ex) { 
            logger.debug("processOrder: "+ex.getMessage()); 
        }
    }
    
    /** Starts order handling thread */
    public void start() {
        if(thread==null)
            thread = new Thread(instance, "Order Handler");
        if(!running) {
            running = true;
            thread.start();
        }
    }
    
    /**
     * Adds new order from client to the database.
     * The order.id is ignored and automatically assigned in the database.
     * @param order Order to be inserted
     */
    public void newOrder(OrderBean order) {
        logger.debug("New order: "+order);
        start();
        // save in database (and calculate the ID) with status "Queued"
        order.setStatus("Queued");
        logger.trace("New order: saving to DB...");
        try {
            Connection c = Database.getInstance().getConnection();
            try (PreparedStatement ps = c.prepareStatement("insert into orders (order_id, customer_id, order_date, src_router, dst_router, bandwidth, status, order_comment) "
                         + "values (gen_order_id.nextval, ?, sysdate, ?, ?, ?, ?, ?)")) {
                ps.setInt(1, order.getUserId());
                ps.setInt(2, RouterBean.loadByName(order.getSourceRouterName()).getId());
                ps.setInt(3, RouterBean.loadByName(order.getDestinationRouterName()).getId());
                ps.setFloat(4, order.getBandwidth());
                ps.setString(5, "QUEUED");
                ps.setString(6, order.getComment());
                ps.executeUpdate();
            }
            logger.trace("New order: commit");
            c.commit();
            logger.trace("New order: success");
        } catch (Exception ex) {
            logger.debug("newOrder: "+ex.getMessage()); 
        }
    }
    
    /** Stops the order handling thread */
    public void stop() {
        logger.debug("Stopping Order Handler thread");
        if(thread==null) return;
        if(!running) return;
        thread.interrupt();
        thread = null;
    }

    /**
     * Extracts an unprocessed order from the database.
     * @return New order or null if there are no any unprocessed orders.
     */
    private OrderBean extractOrder() {
        logger.trace("Loading order from database");
        OrderBean order = null;
        try {
            Connection c = Database.getInstance().getConnection();
            try (PreparedStatement ps = c.prepareStatement("select /*+ FIRST_ROWS(1) */ order_id, customer_id, to_char(order_date, 'DD.MM.YYYY HH24:MI:SS') as order_date, "
                    + "to_char(completion_date, 'DD.MM.YYYY HH24:MI:SS') as completion_date, src_router, dst_router, bandwidth, status, order_comment "
                         + "from orders where status in ('QUEUED','PROCESSING') and rownum<=1")) {
                ResultSet rs = ps.executeQuery();
                if(rs.next()) {
                    order = new OrderBean();
                    order.setId(rs.getInt("order_id"));
                    order.setUserId(rs.getInt("customer_id"));
                    order.setBandwidth(rs.getFloat("bandwidth"));
                    order.setOrderDate(rs.getString("order_date"));
                    order.setComment(rs.getString("order_comment"));
                    order.setSourceRouterName(RouterBean.loadById(rs.getInt("src_router")).getName());
                    order.setDestinationRouterName(RouterBean.loadById(rs.getInt("dst_router")).getName());
                    order.setStatus(rs.getString("status"));
                }
                rs.close();
            }
        } catch (Exception ex) { 
            logger.trace("Failed to obtain order from the database", ex);
        }
        return order;
    }
}